package gazzera.node.core.plugins.download;

import gazzera.gui.*;
import gazzera.gui.messages.*;
import gazzera.node.core.*;
import gazzera.node.core.messages.*;
import gazzera.node.core.peer.*;
import gazzera.node.core.plugins.shareplugin.*;
import gazzera.utils.*;

import java.io.*;
import java.util.*;


public class DownloadPlugin implements Plugin
{
	private ICore core = null;
	private Downloads downloads = null;
	
	private Map<Node, Download> downloaders = new HashMap<Node, Download>();
	private Set<VerificationTask> verificationTasks = new HashSet<VerificationTask>();
	
	private File partial;
	private File completed;
	
	
    @Override
    public void command(GuiMessage msg) throws Exception
    {
		if (msg instanceof NewDownload)
		{
		    NewDownload cmd = (NewDownload)msg;
		    startNewDownload(cmd);
		}
		else if (msg instanceof CancelDownload)
		{
		    CancelDownload c = (CancelDownload)msg;
			cancelDownload(c);
		}
		else if(msg instanceof Status)
		{
			for (Download d : downloads)
			{
				updateDownload(core.getGui(), d);
			}
		}
    }
    private void cancelDownload(CancelDownload c) throws IOException
    {
		Download d = downloads.getById(c.id);
		if(d==null) return;
		for (VerificationTask t : verificationTasks)
		{
			if(t.fileId.equals(d.fileID))
			{
				t.interrupt();
			}
		}
		
		// tell the node to stop sending!
		// and remove them from downloaders
		List<Node> dwnldrs = new ArrayList<Node>(downloaders.keySet());
		for (Node n : dwnldrs)
		{
		    if(downloaders.get(n) == d)
		    {
				n.enqueue(new NotInterested(d.fileID));
				downloaders.remove(n);
		    }
		}
		
		
		d.terminate(false, null);
		
		downloads.remove(d);
		core.getGui().pass(new RemoveDownload(d.id, false));
	
    }
    
    
	public File getCompletedFileFolder()
	{
		return completed;
	}

    private void startNewDownload(NewDownload cmd)
    {
    	// controls to see if we can start this download
    	if(downloads.isDownloading(cmd.fileId))
    	{
    		core.getGui().pass(new Log("Already downloading"));
    		return;
    	}
    	
    	try
		{
    	    		Download d = new Download(cmd.fileId, cmd.name, cmd.size, partial, "subfolder");
			downloads.add(d);
			
			updateDownload(core.getGui(), d);
			if(d.fileID instanceof Hash)
			{
			    ChunkSearch cs = new ChunkSearch(d.fileID, d.size);
			    core.broadcast(cs);
			}
			else if(d.fileID instanceof UserFileID)
			{
			    // we only know user and path
			    Node owner = core.find(((UserFileID)d.fileID).getUserHash());
			    // do we have the owner connected ?
			    if(owner!= null)
			    {
				// ask for his file
				ChunkSearch cs = new ChunkSearch(d.fileID, d.size);
				owner.enqueue(cs);
			    }
			    
			}
		}
		catch (IOException e)
		{
			core.logErr(e);
			core.getGui().pass(new Log("can't start download: "+e.getMessage()));
		}
    	
    }

    @Override
    public void dataPumpTerminated(Node node, DataPumpSource src) throws Exception
    {
	
    }

    @Override
    public void finish() throws Exception
    {
    	downloads.save();
    	for (Download d : downloads)
		{
			d.save();
		}
    }
    
    @Override
    public void init(ICore core) throws Exception
    {
    	this.core = core;
    	
	partial = new File(core.getConfig().getMainFolder(), "partial");
	partial.mkdirs();
	completed = new File(core.getConfig().getMainFolder(), "completed");
	completed.mkdirs();

    	
    	downloads = new Downloads(core.getConfig());
    	downloads.load();    	
    }

    @Override
    public void message(Node node, Message msg) throws Exception
    {
    	if(msg instanceof ChunksAvaiable)
    	{
    		ChunksAvaiable ca = (ChunksAvaiable)msg;
    		replyToChunkAvaiable(node, ca);
    	}
    	else if(msg instanceof ReadyToSend)
    	{
    		ReadyToSend ca = (ReadyToSend)msg;
    		replyToReadyToSend(node, ca);
    	}
    	else if(msg instanceof ChunkData)
    	{
			ChunkData ca = (ChunkData) msg;
			data(node, ca);
    	}
    	else if(msg instanceof FileHash)
    	{
    		FileHash ca = (FileHash) msg;
			replyToFileHash(node, ca);
    	}
    }
	private void replyToFileHash(Node node, FileHash ca) throws IOException 
	{
		// we are notified that i file we only know by its UserFileID got hashed and we can now promote to hash
		if(!node.getHash().equals(ca.userFileId.getUserHash()))
		{
			throw new RuntimeException("Receiving FileHash from wrong person "+node.getHash()+" "+ca.userFileId.getUserHash());
		}
		
		Download d = downloads.get(ca.userFileId);
		if(d!=null)
		{
			if(downloaders.containsValue(d))
			{
				throw new RuntimeException("Receiving FileHash but the file is being downloaded right now!! "+ca.userFileId.getFilePath()+" "+ca.userFileId.getUserHash() );
			}
			d.fileID = ca.hash;
			d.save();
		}
		else
		{
			System.out.println("FileHash for a file we aren't downloading");
		}
		
		
	}
	private void data(Node node, ChunkData ca) throws Exception, IOException
	{
		Download d = downloaders.get(node);
		if( (d != null) && (d.phase == DownloadStatus.DOWNLOADING))
		{
			boolean chunkTerminated = d.writeData(node, ca.data);
			
			long millis = System.currentTimeMillis();
			if((millis-d.lastNotifiedTime)>1000)
			{
				d.tick(millis);
				updateDownload(core.getGui(), d);
			}
			
			if (chunkTerminated)
			{
				d.chunkDownloadTerminated(node);
				downloaders.remove(node);
				
				d.speed = 0;
				updateDownload(core.getGui(), d);
				
				if (d.isCompleted())
				{
					downloadCompleted(d);
				}
				else
				{
					// search some more chunks
					node.enqueue(new Enqueue(d.fileID));
				}
			}
		} else
		{
			System.out.println("ChunkData without download!!");
		}
	}
    
    private void verificationCompleted(Download d, boolean valid, Hash calculatedHash) throws IOException
    {
    	if(valid)
    	{
    		File completedFile = d.terminate(true, completed);
    		downloads.remove(d);
    		
    		if(completedFile!=null)
    		{
    			// put on HashDatabase
    			SharePlugin sp = core.getPlugin(SharePlugin.class);
    			sp.addHashedFile(calculatedHash, completedFile);
    		}
    		
    		core.getGui().pass(new RemoveDownload(d.id, true));
    	}
    	else
    	{
    	    System.out.println("HASH INVALID!! "+d.name);
    	    core.getGui().pass(new Log("HASH INVALID!! "+d.name));
    	}
    }
    
	private void downloadCompleted(Download d) throws IOException
	{
		// download completed, start verification
		d.phase = DownloadStatus.VERIFYING;
		VerificationTask t = new VerificationTask(d.fileID, d.getFile());
		core.startTask(t);
		verificationTasks.add(t);
	}

	private void replyToReadyToSend(Node node, ReadyToSend msg) throws Exception
	{
	    
	    if(downloaders.containsKey(node))
	    {
			// 1 download per user
			core.getGui().pass(new Log("Node ready to send but is already in downloaders! "+node));
			return;
	    }
	    
		Download d = downloads.get(msg.fileId);
		if( (d != null) && (d.phase == DownloadStatus.DOWNLOADING))
		{
			// TODO check if avaiable bitset is ok!
				Pair<Integer, Integer> p = d.getChunkToStart(msg.avaiable);
				if (p == null)
				{
					// nothing needed in avaiable
					node.enqueue(new NotInterested(msg.fileId));
				}
				else
				{
					// let's prepare to receive!!
					int chunkIdx = p.a;
					int offset = p.b;
					d.startChunk(node, chunkIdx, offset);
					node.enqueue(new StartToSend(msg.fileId, chunkIdx, offset));
					downloaders.put(node, d);
				}

		} else
		{
			node.enqueue(new NotInterested(msg.fileId));
		}

	}
    
	private void replyToChunkAvaiable(Node node, ChunksAvaiable ca)
	{
		Download d = downloads.get(ca.fileId);
		if( (d != null) && (d.phase == DownloadStatus.DOWNLOADING))
		{
			// TODO check if avaiable bitset is ok!
			if(ca.avaiable.equals(new BitSet(0)))
			{
				// let's enqueue!
				node.enqueue(new Enqueue(d.fileID));
			}
		}
		
	}

    @Override
    public void nodeConnected(Node node) throws Exception
    {
    	for (Download d : downloads)
    	{
    	    if(d.phase == DownloadStatus.DOWNLOADING)
    	    {
    	    	boolean request = false;
    	    	if (d.fileID instanceof Hash) 
    	    	{
    	    		request = true;
				}
    	    	else if (d.fileID instanceof UserFileID)
    	    	{
    	    		// if it's a UserFileID we request only if the connected user is the owner of the file
    	    		request = ((UserFileID)d.fileID).getUserHash().equals(node.getHash());
    	    	}
    	    	
    	    	if(request)
    	    	{
    	    		ChunkSearch cs = new ChunkSearch(d.fileID, d.size);
    	    		node.enqueue(cs);
    	    	}
    	    }
    	}
    }

    @Override
    public void nodeDisconnected(Node node) throws Exception
    {
    	if(downloaders.containsKey(node))
    	{
    		Download d = downloaders.get(node);
    		d.chunkDownloadTerminated(node);
    		d.speed = 0;
    		updateDownload(core.getGui(), d);
    		if(d.isCompleted())
    		{
    		    // TODO ?????
    		    core.getGui().pass(new Log("Download completo!!"));
    		}
    		downloaders.remove(node);
    	}
    }

    @Override
    public void start() throws Exception
    {
    	
    	for (Download d : downloads)
		{
    		if (d.isCompleted())
			{
				downloadCompleted(d);
			}
		}
		
    }

    @Override
    public void taskTerminated(Task task, boolean interrupted) throws Exception
    {
    	if (!interrupted)
    	{
    		if(task instanceof VerificationTask)
    		{
    			VerificationTask t = (VerificationTask)task;
    			verificationTasks.remove(t);
    			Download d = downloads.get(t.fileId);
    			if(d!=null)
    			{
    				verificationCompleted(d, t.valid, t.calculatedHash);
    			}
    		}
    	}
    }
    
    public static void updateDownload(GuiInterface gui, Download d)
    {
    	DownloadUpdate m = new DownloadUpdate(d.id, d.fileID, d.name, d.size, d.downloaded, d.speed);
    	gui.pass(m);
    }
    

	
}
